Inter-Process Communication (IPC) হলো বিভিন্ন প্রসেসের মধ্যে যোগাযোগ ও সমন্বয় করার একটি গুরুত্বপূর্ণ পদ্ধতি। মৌলিক IPC প্রযুক্তিগুলোর পাশাপাশি, কিছু উন্নত IPC প্রযুক্তি রয়েছে, যা জটিল অ্যাপ্লিকেশনগুলির জন্য আরও কার্যকরী এবং স্কেলেবল যোগাযোগ ব্যবস্থা প্রদান করে। এখানে কিছু উন্নত IPC প্রযুক্তি সম্পর্কে বিস্তারিত আলোচনা করা হলো।
Sockets হলো একটি প্রযুক্তি যা নেটওয়ার্কের মাধ্যমে প্রসেসগুলির মধ্যে যোগাযোগের জন্য ব্যবহৃত হয়। এটি ক্লায়েন্ট-সার্ভার আর্কিটেকচারের ভিত্তিতে কাজ করে।
প্রকার:
ব্যবহার: ওয়েব অ্যাপ্লিকেশন, নেটওয়ার্ক সার্ভিস, এবং ডিস্ট্রিবিউটেড সিস্টেমে ব্যবহৃত হয়।
RPC হলো একটি প্রযুক্তি যা একটি কম্পিউটার প্রোগ্রামকে একটি অন্য কম্পিউটার (সার্ভার) এ থাকা ফাংশন বা সাবরুটিনকে কল করার অনুমতি দেয়।
কাজের প্রক্রিয়া: ক্লায়েন্ট একটি অনুরোধ পাঠায় এবং সার্ভার সেই অনুরোধটি প্রক্রিয়া করে একটি প্রতিক্রিয়া ফেরত দেয়।
ব্যবহার: ডিস্ট্রিবিউটেড অ্যাপ্লিকেশন, মাইক্রোসার্ভিস আর্কিটেকচার, এবং ক্লায়েন্ট-সার্ভার মডেলে।
Message brokers হলো একটি মিডলওয়্যার সমাধান যা ডিস্ট্রিবিউটেড সিস্টেমগুলির মধ্যে বার্তাবিনিময়কে সহজ করে। এটি সার্ভিসগুলিকে একে অপরের থেকে বিচ্ছিন্ন করে।
উদাহরণ: RabbitMQ, Apache Kafka, ActiveMQ।
সুবিধা:
ব্যবহার: ইভেন্ট-ড্রিভেন আর্কিটেকচার, মাইক্রোসার্ভিসে, এবং পাবলিশ-সাবস্ক্রাইবার মডেলে।
Signals হলো একটি IPC পদ্ধতি যা প্রসেসগুলিকে asynchronous নোটিফিকেশন প্রদান করে। এটি একটি প্রসেসকে অন্য একটি প্রসেসের ঘটনার সম্পর্কে জানাতে ব্যবহৃত হয়।
সাধারণ সিগন্যাল: SIGINT, SIGTERM, SIGUSR1, SIGUSR2।
ব্যবহার: প্রসেস ব্যবস্থাপনা, ত্রুটি হ্যান্ডলিং, এবং রিসোর্স ব্যবস্থাপনা।
Memory-mapped files একাধিক প্রসেসকে একটি ফাইলকে তাদের ঠিকানা স্পেসে ম্যাপ করতে দেয়, যাতে তারা সহজে ডেটা শেয়ার করতে পারে।
মেকানিজম: একটি প্রসেসে করা পরিবর্তনগুলি অন্য প্রসেসের জন্য দৃশ্যমান হয়।
সুবিধা: দ্রুত ডেটা শেয়ারিং এবং মেমোরি ব্যবহারে সাশ্রয়।
ব্যবহার: উচ্চ-পারফরম্যান্স অ্যাপ্লিকেশন, বড় ডেটাসেট, এবং রিয়েল-টাইম সিস্টেম।
Futures and promises হল সিঙ্ক্রোনাইজেশন কনস্ট্রাক্ট, যা এক থ্রেডকে অন্য থ্রেডের কার্যক্রম সম্পন্ন হওয়ার জন্য সংকেত দেয়।
মেকানিজম: একটি প্রমিজ একটি মানের জন্য প্লেসহোল্ডার হিসাবে কাজ করে যা ভবিষ্যতে উপলব্ধ হবে।
ব্যবহার: অ্যাসিঙ্ক্রোনাস প্রোগ্রামিং, একাধিক কার্যক্রম পরিচালনা, এবং টাস্কের মধ্যে নির্ভরতাগুলি পরিচালনা।
Event-driven architectures ইভেন্টগুলির মাধ্যমে যোগাযোগ তৈরি করে। উপাদানগুলি ইভেন্ট তৈরি করে এবং অন্যান্য উপাদানগুলি সেগুলি শুনতে পারে এবং প্রতিক্রিয়া জানাতে পারে।
মেকানিজম: ইভেন্ট লুপ এবং কলব্যাক ব্যবহার করে অ্যাসিঙ্ক্রোনাস ইভেন্টগুলি পরিচালনা করা হয়।
ব্যবহার: GUI অ্যাপ্লিকেশন, মাইক্রোসার্ভিস, এবং উচ্চ-প্রতিক্রিয়াশীল সিস্টেম।
Distributed Shared Memory একটি বিমূর্তকরণ যা একটি প্রোগ্রামকে ডিস্ট্রিবিউটেড সিস্টেমে শেয়ার করা ডেটার অ্যাক্সেস করতে দেয় যেন এটি একটি একক শেয়ারড মেমোরির মতো কাজ করে।
মেকানিজম: এটি বিভিন্ন কৌশল দ্বারা বাস্তবায়িত হতে পারে, যেমন পৃষ্ঠা-ভিত্তিক, অবজেক্ট-ভিত্তিক, বা লক-ভিত্তিক শেয়ারিং।
ব্যবহার: ডিস্ট্রিবিউটেড কম্পিউটিং পরিবেশ এবং একাধিক নোডের মধ্যে ডেটার শেয়ারিং।
ডেটাবেস ব্যবহার করে IPC এর মাধ্যমে বিভিন্ন প্রসেসের মধ্যে যোগাযোগ হয়, যেখানে একাধিক প্রসেস একটি সাধারণ ডেটাবেসে পড়া এবং লেখা করে।
মেকানিজম: বিভিন্ন প্রসেস ডেটাবেসের টেবিলগুলিতে CRUD (Create, Read, Update, Delete) অপারেশন করতে পারে।
সুবিধা: বার্তাগুলির স্থায়িত্ব এবং কার্যকারিতা প্রদান করে।
ব্যবহার: ওয়েব অ্যাপ্লিকেশন, এন্টারপ্রাইজ অ্যাপ্লিকেশন, এবং ডিস্ট্রিবিউটেড সিস্টেম।
উন্নত IPC প্রযুক্তিগুলি সমান্তরাল এবং বিতরণকৃত সিস্টেমে কার্যকরী যোগাযোগ এবং সমন্বয় নিশ্চিত করতে অপরিহার্য। এই প্রযুক্তিগুলি বিভিন্ন পরিস্থিতিতে উপযুক্ত, এবং সঠিকভাবে ব্যবহার করা হলে, তারা সফটওয়্যার প্রকল্পের কার্যকারিতা এবং ব্যবহারকারীর অভিজ্ঞতা উন্নত করতে সাহায্য করে। এই প্রযুক্তিগুলি সম্পর্কে ভালোভাবে জানলে ডেভেলপাররা আরও উন্নত ও কার্যকরী অ্যাপ্লিকেশন তৈরি করতে সক্ষম হবেন।
Multithreading এবং IPC (Inter-Process Communication) হল সফটওয়্যার ডেভেলপমেন্টের দুটি গুরুত্বপূর্ণ ধারণা, যা একটি অ্যাপ্লিকেশনের কার্যক্ষমতা ও দক্ষতা বৃদ্ধি করতে সহায়ক। যদিও উভয় প্রযুক্তিই প্রসেসের মধ্যে কার্যক্রম পরিচালনার জন্য ব্যবহৃত হয়, তাদের উদ্দেশ্য এবং কাজের পদ্ধতি ভিন্ন।
Multithreading হলো একটি প্রসেসের মধ্যে একাধিক থ্রেডের পাশাপাশি কার্যকরী হওয়ার প্রক্রিয়া। প্রতিটি থ্রেড মূল প্রসেসের একটি অংশ এবং তারা একই সম্পদের মধ্যে কার্যকরীভাবে কাজ করে।
১. সমান্তরাল কার্যকলাপ: একাধিক থ্রেডের মাধ্যমে একসাথে একাধিক কাজ সম্পন্ন করা যায়।
২. সম্পদ ভাগাভাগি: সমস্ত থ্রেড একই মেমোরি স্পেস ভাগাভাগি করে, যা ডেটার শেয়ারিং সহজ করে।
৩. দ্রুত প্রতিক্রিয়া: থ্রেডগুলি একে অপরের সাথে দ্রুত যোগাযোগ করতে পারে, যা সাধারণত একটি দ্রুত এবং আরও কার্যকরী অ্যাপ্লিকেশন তৈরি করে।
৪. সফলতা: CPU-এর বেশি দক্ষ ব্যবহার করতে সক্ষম, কারণ একটি প্রসেসের মধ্যে বিভিন্ন কাজ সম্পন্ন করা যায়।
import threading
import time
def print_numbers():
for i in range(1, 6):
print(i)
time.sleep(1)
def print_letters():
for letter in 'abcde':
print(letter)
time.sleep(1)
# Create threads
thread1 = threading.Thread(target=print_numbers)
thread2 = threading.Thread(target=print_letters)
# Start threads
thread1.start()
thread2.start()
# Wait for both threads to complete
thread1.join()
thread2.join()
IPC হলো একটি প্রযুক্তি যা একাধিক প্রসেসের মধ্যে তথ্য আদান-প্রদান করতে ব্যবহৃত হয়। এটি বিভিন্ন পদ্ধতি যেমন Pipes, Message Queues, Shared Memory, Sockets ইত্যাদির মাধ্যমে কাজ করে।
১. ডেটা শেয়ারিং: প্রসেসগুলো একে অপরের সাথে ডেটা শেয়ার করতে পারে, যা মাল্টি-প্রসেস অ্যাপ্লিকেশনগুলির জন্য অপরিহার্য।
২. সিঙ্ক্রোনাইজেশন: বিভিন্ন প্রসেসের মধ্যে সিঙ্ক্রোনাইজেশন নিশ্চিত করে, যাতে তারা একসাথে কাজ করতে পারে।
৩. নামযুক্ত এবং অ্যানোনিমাস: IPC প্রযুক্তি কিছু পদ্ধতিতে নামযুক্ত (যেমন Named Pipes) এবং কিছু অ্যানোনিমাস (যেমন Anonymous Pipes) হয়।
৪. বিভিন্ন প্রযুক্তি: IPC অনেক ধরনের প্রযুক্তি সমর্থন করে, যেমন Message Passing, Shared Memory, Sockets, ইত্যাদি।
import multiprocessing
def worker(queue):
queue.put("Hello from worker!")
if __name__ == "__main__":
queue = multiprocessing.Queue()
process = multiprocessing.Process(target=worker, args=(queue,))
process.start()
message = queue.get()
print(message) # Output: Hello from worker!
process.join()
১. দৃঢ় যোগাযোগ: Multithreading প্রক্রিয়া সাধারণত একই প্রসেসের মধ্যে চলে এবং শেয়ার করা ডেটার জন্য সমন্বয় করতে IPC ব্যবহার করতে পারে।
২. অপারেশন সংহতি: একাধিক থ্রেড একই সময়ে কাজ করে, যেখানে IPC অন্যান্য প্রসেসের সাথে যোগাযোগের জন্য ব্যবহৃত হয়।
৩. দ্রুত কার্যকারিতা: Multithreading-এর মাধ্যমে প্রসেসের মধ্যে উচ্চ কার্যকারিতা এবং IPC-এর মাধ্যমে প্রসেসগুলির মধ্যে তথ্য আদান-প্রদান করা যায়।
৪. সিস্টেমের কার্যক্ষমতা: উভয় প্রযুক্তি মিলিয়ে একটি সফটওয়্যার সিস্টেমের কার্যক্ষমতা এবং গুণগত মান উন্নত করতে সহায়ক।
Multithreading এবং IPC উভয়ই সফটওয়্যার ডেভেলপমেন্টের গুরুত্বপূর্ণ উপাদান। Multithreading সফটওয়্যারের মধ্যে বিভিন্ন কাজের সমান্তরাল প্রক্রিয়া চালাতে সহায়ক, যেখানে IPC বিভিন্ন প্রসেসের মধ্যে তথ্য আদান-প্রদান নিশ্চিত করে। উভয় প্রযুক্তির সঠিক ব্যবহার একটি শক্তিশালী এবং কার্যকরী সফটওয়্যার অ্যাপ্লিকেশন তৈরি করতে সহায়ক।
Named Semaphores এবং POSIX Semaphores হল UNIX/Linux সিস্টেমে সিঙ্ক্রোনাইজেশন এবং সমন্বয়ের জন্য ব্যবহৃত সেমাফোর প্রকার। এদের মধ্যে কিছু মৌলিক পার্থক্য এবং ব্যবহারিক উদ্দেশ্য রয়েছে। নিচে প্রতিটি সম্পর্কে বিস্তারিত আলোচনা করা হলো:
Named Semaphores হল সেমাফোরের একটি প্রকার যা একটি নাম (identifier) ব্যবহার করে তৈরি হয়। এই নামের মাধ্যমে বিভিন্ন প্রক্রিয়া একই সেমাফোরের সাথে যোগাযোগ করতে পারে।
#include <stdio.h>
#include <stdlib.h>
#include <semaphore.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
int main() {
// Create a named semaphore
sem_t *sem = sem_open("/mysem", O_CREAT, 0644, 1);
// Wait (decrement the semaphore)
sem_wait(sem);
// Critical section
printf("In critical section\n");
// Post (increment the semaphore)
sem_post(sem);
// Close the semaphore
sem_close(sem);
// Unlink the semaphore
sem_unlink("/mysem");
return 0;
}
POSIX Semaphores হল সেমাফোরের একটি প্রকার যা POSIX (Portable Operating System Interface) মানদণ্ড অনুযায়ী কাজ করে। এটি Named Semaphores এবং Unnamed Semaphores উভয়েরই অন্তর্ভুক্ত।
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
sem_t sem;
void* thread_func(void* arg) {
sem_wait(&sem); // Wait (decrement the semaphore)
// Critical section
printf("Thread in critical section\n");
sem_post(&sem); // Post (increment the semaphore)
return NULL;
}
int main() {
pthread_t threads[5];
// Initialize unnamed semaphore
sem_init(&sem, 0, 1);
for (int i = 0; i < 5; i++) {
pthread_create(&threads[i], NULL, thread_func, NULL);
}
for (int i = 0; i < 5; i++) {
pthread_join(threads[i], NULL);
}
// Destroy the semaphore
sem_destroy(&sem);
return 0;
}
বৈশিষ্ট্য | Named Semaphores | POSIX Semaphores |
---|---|---|
নাম | নাম দিয়ে চিহ্নিত হয় | দুটি প্রকার: নামকৃত এবং নামহীন |
ব্যবহার | একাধিক প্রক্রিয়ার মধ্যে শেয়ার করা হয় | স্থানীয় প্রক্রিয়ার মধ্যে অথবা একাধিক প্রক্রিয়ার মধ্যে ব্যবহার করা যায় |
নির্ভরযোগ্যতা | সিস্টেমের মধ্যে গ্লোবাল অ্যাক্সেস | নির্ভরযোগ্য, তবে স্থানীয় এবং গ্লোবাল উভয়েই কাজ করতে পারে |
রিসোর্স পরিচালনা | ফাইল সিস্টেমে সংরক্ষিত | মেমরির মধ্যে পরিচালনা করা হয় |
ফাংশনালিটি | sem_open() , sem_wait() , sem_post() | sem_init() , sem_wait() , sem_post() |
Named Semaphores এবং POSIX Semaphores উভয়ই প্রক্রিয়া সমন্বয়ের জন্য গুরুত্বপূর্ণ সেমাফোর প্রযুক্তি। Named Semaphores একটি নামের মাধ্যমে গ্লোবাল অ্যাক্সেস প্রদান করে, যেখানে POSIX Semaphores স্থানীয়ভাবে বা গ্লোবালভাবে ব্যবহৃত হতে পারে। উভয় পদ্ধতিই তাদের নিজস্ব ব্যবহার এবং উপকারিতা রয়েছে, এবং সঠিক পদ্ধতি নির্বাচন আপনার অ্যাপ্লিকেশনের প্রয়োজনীয়তার উপর নির্ভর করে।
Condition Variables হল একটি সমান্তরাল প্রোগ্রামিং কৌশল যা থ্রেডের মধ্যে সিঙ্ক্রোনাইজেশন করার জন্য ব্যবহৃত হয়। এটি বিশেষভাবে উপকারী যখন একটি থ্রেড অপেক্ষা করে যে একটি শর্ত পূর্ণ হবে, যেমন ডেটার প্রস্তুতি, সম্পদ উপলব্ধতা, বা কোনও ইভেন্টের ঘটনা।
Condition variables সাধারণত mutexes এর সাথে ব্যবহার করা হয়। একটি থ্রেড একটি condition variable-এ "wait" করতে পারে, এবং অন্য একটি থ্রেড condition variable-এ "signal" বা "broadcast" করতে পারে, যখন এটি শর্তটি পূরণ করে।
১. Mutex তৈরি করা: প্রথমে একটি mutex তৈরি করতে হয়, যা condition variable এর সাথে সম্পর্কিত হবে।
২. Condition Variable তৈরি করা: একটি condition variable তৈরি করা হয়।
৩. Wait করা: যখন একটি থ্রেড একটি নির্দিষ্ট শর্তের জন্য অপেক্ষা করে, এটি mutex লক করে এবং condition variable-এ wait করে।
৪. Signal/Broadcast করা: যখন অন্য একটি থ্রেড শর্ত পূরণ করে, তখন এটি condition variable-এ signal বা broadcast করে।
৫. Mutex আনলক করা: wait করার পর, mutex আনলক করতে হয়।
নিচে একটি সাধারণ উদাহরণ দেওয়া হলো যেখানে একটি producer-consumer সমস্যা সমাধান করা হয়েছে।
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#define BUFFER_SIZE 5
int buffer[BUFFER_SIZE]; // শেয়ার্ড বাফার
int count = 0; // বাফারের বর্তমান সংখ্যা
pthread_mutex_t mutex; // মিউটেক্স
pthread_cond_t cond_full; // বাফার পূর্ণ হলে সংকেত
pthread_cond_t cond_empty; // বাফার খালি হলে সংকেত
void* producer(void* arg) {
for (int i = 0; i < 10; i++) {
pthread_mutex_lock(&mutex); // মিউটেক্স লক করা
while (count == BUFFER_SIZE) { // যদি বাফার পূর্ণ হয়
pthread_cond_wait(&cond_full, &mutex); // অপেক্ষা করা
}
buffer[count] = i; // নতুন আইটেম যোগ করা
printf("Produced: %d\n", i);
count++; // সংখ্যা বাড়ানো
pthread_cond_signal(&cond_empty); // খালি সংকেত পাঠানো
pthread_mutex_unlock(&mutex); // মিউটেক্স আনলক করা
}
return NULL;
}
void* consumer(void* arg) {
for (int i = 0; i < 10; i++) {
pthread_mutex_lock(&mutex); // মিউটেক্স লক করা
while (count == 0) { // যদি বাফার খালি হয়
pthread_cond_wait(&cond_empty, &mutex); // অপেক্ষা করা
}
int item = buffer[count - 1]; // আইটেম নেওয়া
printf("Consumed: %d\n", item);
count--; // সংখ্যা কমানো
pthread_cond_signal(&cond_full); // পূর্ণ সংকেত পাঠানো
pthread_mutex_unlock(&mutex); // মিউটেক্স আনলক করা
}
return NULL;
}
int main() {
pthread_t prod_thread, cons_thread;
pthread_mutex_init(&mutex, NULL); // মিউটেক্স শুরু করা
pthread_cond_init(&cond_full, NULL); // পূর্ণ সংকেত শুরু করা
pthread_cond_init(&cond_empty, NULL); // খালি সংকেত শুরু করা
pthread_create(&prod_thread, NULL, producer, NULL); // উৎপাদক থ্রেড তৈরি করা
pthread_create(&cons_thread, NULL, consumer, NULL); // ভোক্তা থ্রেড তৈরি করা
pthread_join(prod_thread, NULL); // উৎপাদক থ্রেডের সমাপ্তি অপেক্ষা করা
pthread_join(cons_thread, NULL); // ভোক্তা থ্রেডের সমাপ্তি অপেক্ষা করা
pthread_mutex_destroy(&mutex); // মিউটেক্স ধ্বংস করা
pthread_cond_destroy(&cond_full); // পূর্ণ সংকেত ধ্বংস করা
pthread_cond_destroy(&cond_empty); // খালি সংকেত ধ্বংস করা
return 0;
}
১. শেয়ার্ড বাফার: এখানে একটি বাফার তৈরি করা হয়েছে, যেখানে উৎপাদক এবং ভোক্তা ডেটা বিনিময় করে।
২. মিউটেক্স: pthread_mutex_t mutex
ব্যবহার করে শেয়ার্ড ডেটার সুরক্ষা নিশ্চিত করা হয়।
৩. Condition Variables: cond_full
এবং cond_empty
ব্যবহৃত হয়েছে, যা উৎপাদক এবং ভোক্তার মধ্যে সিঙ্ক্রোনাইজেশন নিশ্চিত করে।
৪. Wait এবং Signal: উৎপাদক যখন বাফার পূর্ণ থাকে তখন অপেক্ষা করে, এবং ভোক্তা যখন বাফার খালি থাকে তখন অপেক্ষা করে। সিগন্যাল পাঠিয়ে তারা একে অপরকে জানায় যে তারা ডেটা পাঠাতে বা গ্রহণ করতে প্রস্তুত।
Condition Variables হল একটি শক্তিশালী সিঙ্ক্রোনাইজেশন কৌশল যা থ্রেডগুলির মধ্যে অভিগম্যতা নিয়ন্ত্রণ করতে ব্যবহৃত হয়। সঠিকভাবে ব্যবহার করলে এটি প্রোগ্রামিংয়ে সমান্তরাল কার্যক্রমের নিরাপত্তা এবং কার্যকারিতা নিশ্চিত করতে সহায়ক। Condition variables বিশেষ করে উৎপাদক-ভোক্তা সমস্যা এবং অন্যান্য জটিল থ্রেডিং সমস্যাগুলিতে ব্যাপকভাবে ব্যবহৃত হয়।
Inter-Process Communication (IPC) হল একটি গুরুত্বপূর্ণ প্রযুক্তি যা একাধিক প্রক্রিয়ার মধ্যে তথ্য বিনিময় এবং সমন্বয় করার জন্য ব্যবহৃত হয়। উচ্চ কার্যক্ষমতা সম্পন্ন IPC ডিজাইন তৈরির জন্য কয়েকটি কৌশল এবং নীতি অনুসরণ করা যেতে পারে। নিচে উচ্চ কার্যক্ষমতা সম্পন্ন IPC ডিজাইনের মূল দিকগুলি আলোচনা করা হলো।
বিভিন্ন IPC প্রযুক্তির মধ্যে কিছু হলো:
from multiprocessing import Process, Value
import time
def f(n):
for _ in range(5):
time.sleep(1)
n.value += 1
print(f'Incremented to {n.value}')
if __name__ == '__main__':
num = Value('i', 0) # Shared memory integer
p = Process(target=f, args=(num,))
p.start()
p.join()
print(f'Final Value: {num.value}')
multiprocessing
মডিউল ব্যবহার করে একটি শেয়ার্ড ভ্যালু তৈরি করা হয়।উচ্চ কার্যক্ষমতা সম্পন্ন IPC ডিজাইন তৈরির জন্য সঠিক পদ্ধতি এবং কৌশলগুলি প্রয়োগ করা অপরিহার্য। শেয়ার্ড মেমরি ব্যবহার, সঠিক নেটওয়ার্ক সেগমেন্টেশন, প্রোটোকল অপ্টিমাইজেশন এবং সিঙ্ক্রোনাইজেশন কৌশলগুলি কার্যক্ষমতা বৃদ্ধি করতে সাহায্য করে। সঠিকভাবে ডিজাইন এবং টেস্ট করা হলে, IPC ডিজাইনগুলি একটি স্থিতিশীল এবং কার্যকরী সিস্টেম তৈরি করতে সক্ষম।
Read more